#!/usr/local/bin/node

// OpenJSCAD.org CLI interface, written by Rene K. Mueller <spiritdude@gmail.com>
// License: GPLv2
//
version = '0.010';
//
// Description:
//   openjscad <file> [-o <stl>]
// e.g.
//   openjscad test.jscad                    
//   openjscad test.jscad -o test.stl
//   openjscad test.jscad -o test.amf
//   openjscad test.scad -o testFromSCAD.jscad
//   openjscad test.scad -o test.stl
//   openjscad test.stl -o test2.stl      # reprocessed: stl -> jscad -> stl
//   openjscad test.amf -o test2.jscad    
//   openjscad test.jscad -of amf
//   openjscad test.jscad -of stl
//   openjscad name_plate.jscad --name "Just Me" --title "CEO" -o amf test.amf
//
// History:
// 2013/04/25: 0.010: support of params passed to main()
// 2013/04/12: 0.008: reimplement parseAMF without jquery
// 2013/04/11: 0.007: support of alpha for AMF addded, bumping version
// 2013/04/05: 0.006: support of AMF added, requires node 0.8.1+
// 2013/03/25: 0.005: more sanity check on input and local installation support
// 2013/03/18: 0.004: STL .stl (binary & ascii) support (experimental via openscad.js)
// 2013/03/18: 0.003: OpenSCAD .scad support by Gary Hodgson's openscad-openjscad-translator module
// 2013/03/02: 0.002: proper installation of the dependencies (csg.js & openscad.js) so openjscad can be used properly
// 2013/03/01: 0.001: initial version, with base function from openscad.jscad
//

me = 'cli';
global.time = new Date();
gMainParam = {};

var fs = require('fs');
var vm = require('vm');

//include('./openscad.js');         // later

global.lib = '/usr/local/lib/openjscad/';             // for now hard-coded
global.nodeModules = '/usr/local/lib/node_modules/';  // for now hard-coded too

//if (!fs.existsSync(global.lib))           // requires node 0.10.1 
//   global.lib = './';
if(!fs.statSync(global.lib).isDirectory())  // same but works with 0.6.1+
   global.lib = './';

var CSG = require(lib+'./csg.js').CSG;
var CAG = require(lib+'./csg.js').CAG;  // any way to include CSG & CAG in once?

//require(lib+'./OpenJsCad/openjscad.js'); // make it a nodejs module (later)

// hint: https://github.com/substack/node-optimist
//       https://github.com/visionmedia/commander.js
//
//process.argv.forEach(function (val, index, array) {
//  console.log(index + ': ' + val);
//});

var args = process.argv.splice(2);
var meta = [];

meta.producer = "OpenJSCAD "+me.toUpperCase()+" "+version;
meta.date = new Date();

var formatName = { 
   jscad: "OpenJSCAD.org Source", 
   js: "JavaScript Source", 
   stl: "STereoLithography, ASCII", 
   stla: "STereoLithography, ASCII", 
   stlb: "STereoLithography, Binary", 
   amf: "Additive Manufacturing File Format" 
};

var inf = args[0];
if(inf==null||inf.length<=0||!fs.statSync(inf).isFile()) {
   console.log("USAGE openjscad ("+me.toUpperCase()+" "+version+"): <file> [-of <format>] [-o <output>]\n\t<file>: <name>.jscad, .scad, .stl or .amf\n\t<output>: <name>.jscad, .stl or .amf\n\t<format>: 'jscad', 'stl' (default), 'stla' (STL ASCII), 'stlb' (STL Binary), 'amf'");
   process.exit(1);
}

if(inf.match(/\.(jscad|js|scad|stl|amf)$/i)) {
   inFormat = RegExp.$1;
} else {
   console.log("ERROR: only jscad, scad, stl or amf as input format");
   process.exit(1);
}

var outFormat, inFormat;          
var outf;

for(var i=1; i<args.length; i++) {
   if(args[i]=='-of') {                         // -of <format>
      outFormat = args[++i];

   } else if(args[i].match(/^-o(\S.+)/)) {      // -o<output>
      outf = args[i];
      outf = outf.replace(/^\-o(\S+)$/,'$1');
      
   } else if(args[i]=='-o') {                   // -o <output>
      outf = args[++i];

   } else if(args[i].match(/^--(\w+)=(.*)/)) { // params for main()
      gMainParam[RegExp.$1] = RegExp.$2;

   } else if(args[i].match(/^--(\w+)$/)) {      // params for main()
      gMainParam[RegExp.$1] = args[++i];

   } else {
      console.log("ERROR: bad argument/switch <"+arg[i]+">");
      process.exit(1);
   }
}

if(!outFormat&&!outf) {
   outFormat = 'stl';
}

//console.log("reading "+inf);
var src = fs.readFileSync(inf,inf.match(/\.stl$/i)?"binary":"UTF8");
var scad = fs.readFileSync(lib+'./openscad.js');
var inc = [];

//var csg = sphere(1);          // -- basic test
//var csg = require(file).main; // -- treating .jscad as module? later perhaps

if(!outFormat&&outf&&outf.length&&outf.match(/\.(jscad|js|stl|amf)$/)) {         // output filename set
   outFormat = RegExp.$1;

} else if(!outFormat&&outf&&outf.length) {                                    // output filename isn't valid
   console.log("ERROR: bad output filename <"+outf+">, only .jscad, .stl or .amf as output format");
   process.exit(1);

} else if(outFormat.match(/(jscad|js|stl|stla|stlb|amf)/i)) {  // output format defined?
   var ext = RegExp.$1;
   if(!outf) {                                              // unless output filename not set, compose it
      ext = ext.replace(/stl[ab]/,'stl');                      // drop [ab] from stl
      outf = inf;
      outf = outf.replace(/\.([^\.]+)$/,"."+ext);              // compose output filename
   }
   
} else {
   console.log("ERROR: output format <"+outFormat+">, only jscad, stl or amf as output format");
   process.exit(1);
}

console.log("converting "+inf+" -> "+outf+" ("+formatName[outFormat]+")");

// -- include input, and convert into JSCAD source
if(inFormat=='scad') {
   //var scadParser = require('openscad-openjscad-translator');     // npm installed but doesn't find it (crap!)
   var scadParser = require(global.nodeModules+'openscad-openjscad-translator');  // hardcoded is bad, but works
   src = scadParser.parse(src); //    doing the magick
   src = "// producer: OpenJSCAD "+me.toUpperCase()+" "+version+"\n"+src;
   src = "// source: "+outf+"\n\n"+src;
   
} else if(inFormat=='stl') {
   //console.log("converting "+inf+" to jscad");
   var openscad = require(lib+'openscad.js');
   src = openscad.parseSTL(src,inf);
   //src = "// "+outf+" created by openjscad-"+version+" from "+inf+"\n\n"+src;
   
} else if(inFormat=='amf') {
   //console.log("converting "+inf+" to jscad");
   // $ = require(global.nodeModules+'jquery');   // needed for AMF (XML) parsing
   // var openscad = require(lib+'openscad.js');
   // src = openscad.parseAMF(src,inf);
   src = parseAMF(src,inf);
   //src = "// "+outf+" created by openjscad-"+version+" from "+inf+"\n\n"+src;
   
} else {
   // jscad or js
   ;
}

// -- convert from JSCAD into suitable format wanted
if(outFormat=='jscad'||outFormat=='js') {
   out = src;
} else {
   //console.log("render jscad to "+outFormat);
   //console.log(JSON.stringify(gMainParam));

   var csg = eval(src+"\n"+scad+"\nmain(_getParameterDefinitions("+JSON.stringify(gMainParam)+"))\n");    // *.jscad + openscad.js + main()

   if(csg.length) {
      var o = csg[0];
      if(o instanceof CAG) {
         o = o.extrude({offset: [0,0,0.1]});
      }
      for(i=1; i<csg.length; i++) {
         var c = csg[i];
         if(c instanceof CAG) {
            c = c.extrude({offset: [0,0,0.1]});
         }
         o = o.unionForNonIntersecting(c);
      }
      csg = o;
   }
   var out;
   if(outFormat=='amf') {
      out = csg.toAMFString(meta);

   } else if(outFormat=='stlb') {
      out = csg.toStlBinary();                              // not yet supported

   } else if(outFormat=='stl'||outFormat=='stla') {
      out = csg.toStlString();

   } else {
      console.log("ERROR: only jscad, stl or amf as output format");
      process.exit(1);
   }
}

fs.writeFileSync(outf,out,outFormat=='stlb'?"binary":"utf8");

// -- helper functions ---------------------------------------------------------------------------------------

function include(fn) {    
   //console.log(arguments.callee.caller,"include:"+fn);
   if(0) {
      //var script = vm.createScript(fs.readFileSync(fn),fn);
      //script.runInThisContext();
      var script = vm.runInThisContext(fs.readFileSync(fn),fn);
      return script;
   } else if(0) {
      inc.push(fn);
      
   } else {
      var src = fs.readFileSync(fn);
      //console.log("exec",src);
      var r;
      try {
         r = eval(src+scad);
      } catch(e) {
         if(e instanceof SyntaxError) {
            console.log(e.message);
         }
      }
      //echo("result:",r);
      return r;
   }
}

function getXMLContent(s,tag) {
   var match = '</'+tag+'>';
   var m = s.split(new RegExp(match,'m'));
   return m;

   // -- below a more elaborate parsing (not yet working)
   var r = [];            
   var err = '';
   
   match = '<'+tag+'([^>]*)>([^<]*)';
   //match = '<'+tag+'>([^<]*)';
   console.log(tag);
   for(var i=0; i<m.length; i++) {
      //console.log(tag,i,m[i].substr(0,500)+".."+m[i].substr(m[i].length-100),m[i].length);
      if(!m[i].length) 
         continue;
      if(m[i].match(new RegExp(match,'m'))) {
         var a = RegExp.$1;
         var t = RegExp.$2;
         var attr = [];
         if(a.length) {
            // processing ' something="this" that="here"' .. (later)
         }
         if(t.length) {
            r.push(t); 
            //r.push({ text: t, attr: attr });
            //console.log(tag+": '"+t+"'");
         } else {
            //console.log(tag+": '"+t+"' (empty)");
         }
      }
   }
   if(r.length==0) {
      err += "no <"+tag+"> found despite finding </"+tag+"> "+(m.length)+" time(s)"
   }
   if(err) {
      console.log("XML parse error: "+err);
   }
   return r;
}

function parseAMF(amf,fn) {      // re-implemented since node-jquery doesn't do parse.XML (great!)
   var vt = [];                  // works: color per volume
   var tt = [];                  // missing: materials neither color per triangle (fix it)
   var co = [];
   var meta = [];

   //parseXML(amf);
   
   var metadata = amf.split(/<\/metadata>/);
   metadata.forEach(function(v,i) {
      if(v.match(/<metadata\s+type="([^"]+)"\s*>(.*)/))
         meta[RegExp.$1] = RegExp.$2;
   });
   
   var object = getXMLContent(amf,'object');
   //var object = amf.split(/<\/object>/);
   object.forEach(function(v,i) {
      var mesh = getXMLContent(v,'mesh');
      //var mesh = v.split(/<\/mesh>/);
      mesh.forEach(function(v,i) {
         var vertices = getXMLContent(v,'vertices');
         //var vertices = v.split(/<\/vertices>/);
         vertices.forEach(function(v,i) {
            var vertex = getXMLContent(v,'vertex');
            //var vertex = v.split(/<\/vertex>/);
            vertex.forEach(function(v,i) {
               var coordinates = getXMLContent(v,'coordinates'); 
               //var coordinates = v.split(/<\/coordinates>/);
               coordinates.forEach(function(v,i) {
                  var xyz = getXMLContent(v,'[xyz]');
                  //var xyz = v.split(/<\/[xyz]/);
                  var coord = [];
                  xyz.forEach(function(v,i) {
                     //console.log("==="+v);
                     if(v.match(/<[xyz]>([^<]+)/)) 
                        coord.push(RegExp.$1);
                     //coord.push(v);
                  });
                  if(coord.length==3) {
                     //console.log("vertex: "+coord);
                     vt.push(coord);
                  }
               });
            });
         });
         //var volume = getXMLContent(v,'volume');
         var volume = v.split(/<\/volume>/);
         volume.forEach(function(v,i) {
            //var triangle = getXMLContent(v,'triangle');
            var triangle = v.split(/<\/triangle>/);
            //var color = getXMLContent(v,'color');
            var color = v.split("<\/color>");
            var rgbv = [];
            color.forEach(function(v,i) {
               //var rgba = getXMLContent(v,'[rgba]');
               var rgba = v.split(/<\/[rgba]>/);
               rgba.forEach(function(v,i) {
                  if(v.match(/<[rgba]>(\S+)/))
                     rgbv.push(RegExp.$1);
                  //rgbv.push(v);
               });
            });
            triangle.forEach(function(v,i) {
               //var faces = getXMLContent(v,'v[123]');
               var faces = v.split(/<\/v[123]>/);
               var fc = [];
               faces.forEach(function(v,i) {
                  if(v.match(/<v[123]>(\d+)/))
                     fc.push(RegExp.$1);
                  //fc.push(v);
               });
               if(fc.length==3) {
                  tt.push(fc);
                  if(rgbv.length) 
                     co[tt.length-1] = rgbv;
               }
            });
         });
      });
   });

   // vt[] has the vertices
   // tt[] has the faces
   var srci = '', err = '';
   var np = 0; 
   
   srci = "\tvar pgs = [];\n";
   
   for(var i=0; i<tt.length; i++) {
      //srci += "\tpgs.push(new CSG.Polygon([\n\t\t";
      srci += "\tpgs.push(PP([\n\t\t";
      for(var j=0; j<tt[i].length; j++) {
         if(tt[i][j]<0||tt[i][j]>=vt.length) {
            if(err.length=='') err += "bad index for vertice (out of range)";
            continue;
         }
         if(j) srci += ",\n\t\t";
         srci += "VV("+vt[tt[i][j]]+")";
      }
      srci += "])";
      if(co[i]) srci += ".setColor("+co[i]+")";
      srci += ");\n";
      np++;
   }
   var src = "";
   for(var k in meta) {
      src += "// AMF."+k+": "+meta[k]+"\n";
   }
   src += "// producer: OpenJSCAD "+me.toUpperCase()+" "+version+" AMF Importer\n";
   src += "// date: "+(new Date())+"\n";
   src += "// source: "+fn+"\n";
   src += "\n";
   
   if(err) src += "// WARNING: import errors: "+err+" (some triangles might be misaligned or missing)\n";
   src += "// objects: 1\n// object #1: polygons: "+np+"\n\n";
   src += "function main() {\n"; 
   src += "\tvar PP = function(a) { return new CSG.Polygon(a); }\n"; 
   src += "\tvar VV = function(x,y,z) { return new CSG.Vertex(new CSG.Vector3D(x,y,z)); }\n";
   //src += vt2jscad(v,f,[],c);
   src += srci;
   src += "\treturn CSG.fromPolygons(pgs);\n}\n";
   return src;
   
}

// preparing XML parsing to JSON (not yet working) to parse AMF natively (without dependency of any library)

function parseXML(xml,fn) {
   var state = 'default';
   var stack = [];
   
   stack.push('root');
   var tag = '';
   
   for(var i=0; i<xml.length; i++) {
      var c = xml.substr(i,1);
      if(c=='<'&&state=='default') {
         tag = '';
         state = 'tagStart';
      } else if(c=='>'&&(state=='tagStart'||state=='tagEnd')) {
         //console.log(tag,state);
         if(state=='tagEnd') {
            stack.pop();
         } else {
            stack.push({tag: tag, content: ''});
         }
         state = 'default';
      } else if(c=='/'&&state=='tagStart') {
         state = 'tagEnd';
      } else if(state=='tagStart'||state=='tagEnd') {
         tag += c;
      } else {
         stack[stack.length-1].content += c;
      }
   }
   if(stack.length) {
      console.log("missing closing tags:");   
      for(var i=0; i<stack.length; i++) {
         console.log(stack[i]);
      }
   }
}
   

